﻿using EA;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UMLSynthesizer.EAUMLSynthesizerPlugin.ExtensionMethods;
using UMLSynthesizer.Tasks;
using UMLSynthesizer.Tasks.DiagramElements;
using UMLSynthesizer.Tasks.DiagramElements.Components;
using UMLSynthesizer.Tasks.DiagramElements.Links;
using UMLSynthesizer.Tasks.DiagramElements.Objects;

namespace UMLSynthesizer.EAUMLSynthesizerPlugin.EAProjects
{
    class ClassDiagramEAProject : EAProjectBase
    {
        #region Declaration

        const string DiagramName = "UML Synthesizer class diagram";
        public const string DisplayName = "Create class diagram";
        const string ExternalElementPackageName = "External Elements";

        const int ElementMargin = 20;
        const int DefaultElementHeight = 70;
        const int DefaultElementWidth = 130;
        const string DependencyConnectorType = "Dependency";
        const string GeneralizationConnectorType = "Generalization";
        const string NoteConnectorType = "NoteLink";
        const string RealizationConnectorType = "Realization";

        #endregion

        #region Fields

        ClassDiagramTask classDiagramTask;
        Diagram classDiagram;
        Package externalElementsRoot;
        Package workingView;
        string rootPackageName;

        #endregion

        #region Public Interface

        /// <summary>
        /// The constructor
        /// </summary>
        public ClassDiagramEAProject(Repository repository)
            : base(repository)
        {
            classDiagramTask = new ClassDiagramTask(RefreshPrint);
        }

        /// <summary>
        /// Start the project
        /// </summary>
        /// <param name="repository">The EA repository object</param>
        public override void StartProject()
        {
            //Select view
            workingView = repository.SelectWorkingView();
            if (workingView == null)
                return;

            classDiagram = workingView.Diagrams.AddNew(DiagramName, "Logical") as Diagram;
            classDiagram.Update();
            workingView.Diagrams.Refresh();

            //Start task
            classDiagramTask.StartTask();
        }

        /// <summary>
        /// Refresh the project
        /// </summary>
        public override void RefreshPrint()
        {
            //Delete generated packages
            for (short i = (short)(workingView.Packages.Count - 1); i >= 0; i--)
            {
                var currentPackage = workingView.Packages.GetAt(i) as Package;
                if (currentPackage.Name == ExternalElementPackageName ||
                    (rootPackageName != null && currentPackage.Name == rootPackageName))
                {
                    workingView.Packages.Delete(i);
                    workingView.Packages.Refresh();
                }
            }
            rootPackageName = null;

            //Create external elements root
            externalElementsRoot = workingView.Packages.AddNew(ExternalElementPackageName, "");
            externalElementsRoot.Update();
            workingView.Packages.Refresh();

            //Build packages
            BuildPackage(classDiagramTask.ElementsToPrint, workingView, classDiagram);

            repository.RefreshModelView(0);
            repository.OpenDiagram(classDiagram.DiagramID);
        }

        #endregion

        #region Private helpers

        /// <summary>
        /// Add an element to a diagram and return the new DiagramObject
        /// </summary>
        /// <param name="diagram">The destination diagram</param>
        /// <param name="element">The element to add</param>
        /// <param name="identifier">The element identifier into the diagram</param>
        /// <param name="x">The x coordinate</param>
        /// <param name="y">The y coordinate</param>
        /// <param name="width">The element width</param>
        /// <param name="height">The element height</param>
        /// <returns>The diagram object</returns>
        static DiagramObject AddElementToDiagram(
            Diagram diagram,
            Element element,
            int? x = null,
            int? y = null,
            int width = DefaultElementWidth,
            int height = DefaultElementHeight)
        {
            DiagramObject diagramElement = diagram.DiagramObjects.AddNew(element.Name, "");
            diagramElement.ElementID = element.ElementID;
            if(x.HasValue)
            {
                diagramElement.left = x.Value;
                diagramElement.right = x.Value + width;
            }
            if (y.HasValue)
            {
                diagramElement.top = y.Value;
                diagramElement.bottom = y.Value - height;
            }
            diagramElement.Update();
            diagram.DiagramObjects.Refresh();
            return diagramElement;
        }
        /// <summary>
        /// Add a link element into the diagram
        /// </summary>
        /// <param name="elementDictionary">The current element dictionary</param>
        /// <param name="linkElement">The element to add</param>
        /// <param name="connectorType">The string type of the connector</param>
        private static void AddLinkElementToDiagram(Dictionary<DiagramElementBase, Element> elementDictionary,
            DiagramLinkBase linkElement, string connectorType)
        {
            Element firstElement = (from elementPair in elementDictionary
                                    where elementPair.Key == linkElement.FirstObject
                                    select elementPair.Value).FirstOrDefault();
            Element secondElement = (from elementPair in elementDictionary
                                     where elementPair.Key == linkElement.SecondObject
                                     select elementPair.Value).FirstOrDefault();
            if (firstElement != null && secondElement != null)
            {
                Connector connector = firstElement.Connectors.AddNew("", connectorType);
                connector.SupplierID = secondElement.ElementID;
                connector.Update();
                firstElement.Connectors.Refresh();
            }
        }

        /// <summary>
        /// The recursive function that builds the EA project structure
        /// </summary>
        /// <param name="elementsToPrint">Elements to add</param>
        /// <param name="currentPackage">Where to add elements</param>
        /// <param name="currentDiagram">Where to visualize elements</param>
        void BuildPackage(IEnumerable<DiagramElementBase> elementsToPrint, Package currentPackage, Diagram currentDiagram)
        {
            Dictionary<DiagramElementBase, Element> elementDictionary = new Dictionary<DiagramElementBase, Element>();
            List<DiagramElementBase> externalElementsToPrint = new List<DiagramElementBase>();

            //setup the diagram
            currentDiagram.StyleEx = "StyleEx: string = ShowNotes=1;";
            currentDiagram.Update();

            //populate the package with internal elements and links
            foreach (var element in elementsToPrint)
            {
                switch (element.Kind)
                {
                    case DiagramElementBase.ElementKinds.Class:
                        //create the class
                        Element classElement = currentPackage.Elements.AddNew((element as ClassObject).Identifier, "Class");
                        elementDictionary.Add(element, classElement);
                        if ((element as ClassObject).IsAbstract)
                            classElement.Abstract = "1";
                        if ((element as ClassObject).Note != null)
                            classElement.Notes = (element as ClassObject).Note;
                        if ((element as ClassObject).Visibility != DiagramElementBase.Visibilities.ND)
                            classElement.Visibility = (element as ClassObject).Visibility.ToString();
                        classElement.Update();
                        currentPackage.Elements.Refresh();

                        //fill the class with attributes and methods
                        SetupAttributes((element as ClassObject).Attributes, classElement);
                        SetupMethods((element as ClassObject).Methods, classElement);

                        //Add to diagram
                        DiagramObject diagramClassElement = AddElementToDiagram(currentDiagram, classElement);

                        break;
                    case DiagramElementBase.ElementKinds.DependencyLink:
                        if (elementDictionary.ContainsKey((element as DiagramLinkBase).FirstObject) &&
                            elementDictionary.ContainsKey((element as DiagramLinkBase).SecondObject))
                            AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, DependencyConnectorType);
                        else
                            externalElementsToPrint.Add(element);
                        break;
                    case DiagramElementBase.ElementKinds.ExternalElement:
                    case DiagramElementBase.ElementKinds.ExternalPackage:
                        externalElementsToPrint.Add(element);
                        break;
                    case DiagramElementBase.ElementKinds.GeneralizationLink:
                        if (elementDictionary.ContainsKey((element as DiagramLinkBase).FirstObject) &&
                            elementDictionary.ContainsKey((element as DiagramLinkBase).SecondObject))
                            AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, GeneralizationConnectorType);
                        else
                            externalElementsToPrint.Add(element);
                        break;
                    case DiagramElementBase.ElementKinds.Interface:
                        //create the interface
                        Element interfaceElement = currentPackage.Elements.AddNew((element as InterfaceObject).Identifier, "Interface");
                        elementDictionary.Add(element, interfaceElement);
                        if ((element as InterfaceObject).Note != null)
                            interfaceElement.Notes = (element as InterfaceObject).Note;
                        if ((element as InterfaceObject).Visibility != DiagramElementBase.Visibilities.ND)
                            interfaceElement.Visibility = (element as InterfaceObject).Visibility.ToString();
                        interfaceElement.Update();
                        currentPackage.Elements.Refresh();

                        //fill the interface with methods
                        SetupMethods((element as InterfaceObject).Methods, interfaceElement);

                        //Add to diagram
                        DiagramObject diagramInterfaceElement = AddElementToDiagram(currentDiagram, interfaceElement);

                        break;
                    case DiagramElementBase.ElementKinds.Package:
                        //create the package
                        Package packageElement = currentPackage.Packages.AddNew((element as PackageObject).Identifier, "");
                        elementDictionary.Add(element, packageElement.Element);
                        if (rootPackageName == null)
                            rootPackageName = (element as PackageObject).Identifier;
                        if ((element as PackageObject).Note != null)
                            packageElement.Notes = (element as PackageObject).Note;
                        packageElement.Update();
                        currentPackage.Packages.Refresh();

                        //create the package's diagram
                        Diagram packageElementDiagram = packageElement.Diagrams.AddNew((element as PackageObject).Identifier, "Logical");
                        packageElementDiagram.Update();
                        packageElement.Diagrams.Refresh();

                        //recursive call on package's children
                        BuildPackage((element as PackageObject).ChildElements, packageElement, packageElementDiagram);

                        //Add to diagram
                        DiagramObject diagramPackageElement = AddElementToDiagram(currentDiagram, packageElement.Element);
                        break;
                    case DiagramElementBase.ElementKinds.RealizationLink:
                        if (elementDictionary.ContainsKey((element as DiagramLinkBase).FirstObject) &&
                            elementDictionary.ContainsKey((element as DiagramLinkBase).SecondObject))
                            AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, RealizationConnectorType);
                        else
                            externalElementsToPrint.Add(element);
                        break;
                }
            }

            //automatic layout
            repository.GetProjectInterface().LayoutDiagramEx(currentDiagram.DiagramGUID, CustomLayoutStyles.lsProgramDefault, 4, 20, 20, false);

            //add external elements and links
            int bottomBorder = -ElementMargin;
            int rightBorder = ElementMargin;
            foreach (DiagramObject diagramObject in currentDiagram.DiagramObjects)
                if (diagramObject.bottom < bottomBorder)
                    bottomBorder = diagramObject.bottom;

            foreach (var element in externalElementsToPrint)
            {
                int tmpBottomBorder = bottomBorder, tmpRightBorder = rightBorder;
                switch (element.Kind)
                {
                    case DiagramElementBase.ElementKinds.DependencyLink:
                        AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, DependencyConnectorType);
                        break;
                    case DiagramElementBase.ElementKinds.ExternalElement:
                    case DiagramElementBase.ElementKinds.ExternalPackage:
                        BuildExternalPackage(element, currentDiagram, elementDictionary, bottomBorder,
                            rightBorder, out tmpBottomBorder, out tmpRightBorder);
                        break;
                    case DiagramElementBase.ElementKinds.GeneralizationLink:
                        AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, GeneralizationConnectorType);
                        break;
                    case DiagramElementBase.ElementKinds.RealizationLink:
                        AddLinkElementToDiagram(elementDictionary, element as DiagramLinkBase, RealizationConnectorType);
                        break;
                }
                rightBorder = tmpRightBorder;
            }

            repository.CloseDiagram(currentDiagram.DiagramID);
        }

        /// <summary>
        /// Build an external diagram inside the current diagram
        /// </summary>
        /// <param name="externalObject">The external element to draw</param>
        /// <param name="currentDiagram">The diagram</param>
        /// <param name="elementDictionary">The element dictionary</param>
        /// <param name="topBorder">The top border</param>
        /// <param name="leftBorder">The left border</param>
        /// <param name="bottomBorder">The bottom border</param>
        /// <param name="rightBorder">The right border</param>
        /// <returns>The z-order of the inserted element</returns>
        int BuildExternalPackage(DiagramElementBase externalObject,
            Diagram currentDiagram,
            Dictionary<DiagramElementBase, Element> elementDictionary,
            int topBorder,
            int leftBorder,
            out int bottomBorder,
            out int rightBorder)
        {
            bottomBorder = rightBorder = 0;

            switch (externalObject.Kind)
            {
                case DiagramElementBase.ElementKinds.ExternalElement:
                    var externalEAElement = SelectExternalElement(externalObject as ExternalElementObject);
                    elementDictionary.Add(externalObject, externalEAElement);

                    //Add margins
                    topBorder -= ElementMargin;
                    leftBorder += ElementMargin;

                    //I don't know why, but sometimes adding an element to diagram it fails
                    //This may be an EA bug
                    DiagramObject elementDiagramObject = null;
                    try
                    {
                        //Add the element to the diagram
                        elementDiagramObject = AddElementToDiagram(currentDiagram, externalEAElement, leftBorder, topBorder);
                    }
                    catch (Exception) { }

                    bottomBorder = topBorder;
                    rightBorder = leftBorder;

                    //if the operation isn't failed
                    if (elementDiagramObject != null)
                    {
                        //Update bottom and right borders
                        bottomBorder -= DefaultElementHeight;
                        rightBorder += DefaultElementWidth;
                        elementDiagramObject.Sequence = 1;
                        elementDiagramObject.Update();
                    }

                    //z-order
                    return 1;
                case DiagramElementBase.ElementKinds.ExternalPackage:
                    int zOrder = 1;
                    var externalEAPackage = SelectExternalPackage(externalObject as ExternalPackageObject);

                    //Add margins
                    topBorder -= ElementMargin;
                    leftBorder += ElementMargin;

                    //Add the package to the diagram
                    var packageDiagramObject = AddElementToDiagram(currentDiagram, externalEAPackage.Element, leftBorder, topBorder);

                    //Recursion on childs
                    bottomBorder = topBorder - DefaultElementHeight;
                    foreach (var childElement in (externalObject as ExternalPackageObject).ChildElements)
                    {
                        int tempBottomBorder, tempRightBorder, tempZOrder;
                        tempZOrder = BuildExternalPackage(childElement, currentDiagram, elementDictionary,
                            topBorder - ElementMargin, leftBorder, out tempBottomBorder, out tempRightBorder);

                        if (tempBottomBorder < bottomBorder)
                            bottomBorder = tempBottomBorder;
                        leftBorder = tempRightBorder;
                        if (zOrder < tempZOrder + 1)
                            zOrder = tempZOrder + 1;
                    }

                    //Update right and bottom borders and fit to content
                    bottomBorder -= ElementMargin;
                    rightBorder = leftBorder + ElementMargin;
                    packageDiagramObject.bottom = bottomBorder;
                    packageDiagramObject.right = rightBorder;
                    packageDiagramObject.Sequence = zOrder;
                    packageDiagramObject.Update();

                    return zOrder;
            }

            return 1;
        }

        /// <summary>
        /// Select and element from the External root package. Create the element
        /// if it doesn't exists
        /// </summary>
        /// <param name="externalElement">The element to select</param>
        /// <returns>The selected element</returns>
        Element SelectExternalElement(ExternalElementObject externalElement)
        {
            if (externalElement == null)
                throw new ArgumentNullException();
            string identifier = externalElement.Identifier;
            if (identifier.Length == 0)
                identifier = "<no name>";

            Element selectedElement = (from Element element in externalElementsRoot.Elements
                                       where element.Name == identifier
                                       select element).FirstOrDefault();
            if (selectedElement == null)
            {
                //create the element
                selectedElement = externalElementsRoot.Elements.AddNew(identifier, "Class");
                selectedElement.Update();
                externalElementsRoot.Elements.Refresh();
            }

            return selectedElement;
        }
        /// <summary>
        /// Select a package from the External root package. Create the package
        /// if it doesn't exists
        /// </summary>
        /// <param name="externalPackage">The package to select</param>
        /// <returns>The selected package</returns>
        Package SelectExternalPackage(ExternalPackageObject externalPackage)
        {
            if (externalPackage == null)
                throw new ArgumentNullException();
            string identifier = externalPackage.Identifier;
            if (identifier.Length == 0)
                identifier = "<no name>";

            Package selectedPackage = (from Package package in externalElementsRoot.Packages
                                       where package.Name == identifier
                                       select package).FirstOrDefault();
            if (selectedPackage == null)
            {
                //create the package
                selectedPackage = externalElementsRoot.Packages.AddNew(identifier, "");
                selectedPackage.Update();
                externalElementsRoot.Packages.Refresh();
            }

            return selectedPackage;
        }
        /// <summary>
        /// Setup attributes in an EA element
        /// </summary>
        /// <param name="attributes">The attribute list</param>
        /// <param name="eaElement">The destination element</param>
        static void SetupAttributes(IEnumerable<AttributeComponent> attributes, Element eaElement)
        {
            foreach (var attribute in attributes)
            {
                EA.Attribute attributeElement = eaElement.Attributes.AddNew(attribute.Identifier, "");
                if (attribute.IsStatic)
                    attributeElement.IsStatic = true;
                if (attribute.Type != null)
                    attributeElement.Type = attribute.Type;
                if (attribute.Visibility != DiagramElementBase.Visibilities.ND)
                    attributeElement.Visibility = attribute.Visibility.ToString();
                attributeElement.Update();
            }
            eaElement.Attributes.Refresh();
        }
        /// <summary>
        /// Setup methods in an EA element
        /// </summary>
        /// <param name="methods">The method list</param>
        /// <param name="eaElement">The destination element</param>
        static void SetupMethods(IEnumerable<MethodComponent> methods, Element eaElement)
        {
            foreach (var method in methods)
            {
                Method methodElement = eaElement.Methods.AddNew(method.Identifier, "");
                if (method.IsAbstract)
                    methodElement.Abstract = true;
                if (method.IsStatic)
                    methodElement.IsStatic = true;
                if (method.Type != null)
                    methodElement.ReturnType = method.Type;
                if (method.Visibility != DiagramElementBase.Visibilities.ND)
                    methodElement.Visibility = method.Visibility.ToString();
                methodElement.Update();
                foreach (var parameter in method.Parameters)
                {
                    Parameter parameterElement = methodElement.Parameters.AddNew(parameter.Identifier, "");
                    if (parameter.Type != null)
                        parameterElement.Type = parameter.Type;
                    parameterElement.Update();
                }
                methodElement.Parameters.Refresh();
            }
            eaElement.Methods.Refresh();
        }

        #endregion
    }
}
